/*
 * Copyright 2012-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zuma.oci.docker.transport;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.zuma.oci.docker.handler.IResponseProcessHandler;
import com.zuma.oci.docker.type.RequestParam;
import com.zuma.oci.io.IOConsumer;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.*;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
import org.apache.hc.core5.http.io.entity.FileEntity;
import org.apache.hc.core5.http.protocol.HttpCoreContext;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Map;

/**
 * Abstract base class for {@link HttpTransport} implementations backed by a
 * {@link HttpClient}.
 *
 * @author Phillip Webb
 * @author Mike Smithson
 * @author Scott Frederick
 * @author Moritz Halbritter
 */
abstract class HttpClientTransport implements HttpTransport {

    static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth";

    private final HttpClient client;

    private final HttpHost host;

    protected HttpClientTransport(HttpClient client, HttpHost host) {
        Assert.notNull(client, "'client' must not be null");
        Assert.notNull(host, "'host' must not be null");
        this.client = client;
        this.host = host;
    }

    /**
     * Perform an HTTP GET operation.
     *
     * @param uri the destination URI
     * @return the operation response
     */
    @Override
    public Response get(URI uri) {
        return execute(new HttpGet(uri));
    }

    /**
     * Perform an HTTP POST operation.
     *
     * @param uri the destination URI
     * @return the operation response
     */
    @Override
    public Response post(URI uri) {
        return execute(new HttpPost(uri));
    }

    /**
     * Perform an HTTP POST operation.
     *
     * @param uri          the destination URI
     * @param registryAuth registry authentication credentials
     * @return the operation response
     */
    @Override
    public Response post(URI uri, String registryAuth) {
        return execute(new HttpPost(uri), registryAuth);
    }

    /**
     * Perform an HTTP POST operation.
     *
     * @param uri         the destination URI
     * @param contentType the content type to write
     * @param writer      a content writer
     * @return the operation response
     */
    @Override
    public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) {
        return execute(new HttpPost(uri), contentType, writer);
    }

    /**
     * Perform an HTTP PUT operation.
     *
     * @param uri         the destination URI
     * @param contentType the content type to write
     * @param writer      a content writer
     * @return the operation response
     */
    @Override
    public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) {
        return execute(new HttpPut(uri), contentType, writer);
    }

    /**
     * Perform an HTTP DELETE operation.
     *
     * @param uri the destination URI
     * @return the operation response
     */
    @Override
    public Response delete(URI uri) {
        return execute(new HttpDelete(uri));
    }

    /**
     * Perform an HTTP HEAD operation.
     *
     * @param uri the destination URI
     * @return the operation response
     */
    @Override
    public Response head(URI uri) {
        return execute(new HttpHead(uri));
    }

    private Response execute(HttpUriRequestBase request, String contentType, IOConsumer<OutputStream> writer) {
        request.setEntity(new WritableHttpEntity(contentType, writer));
        return execute(request);
    }

    private Response execute(HttpUriRequestBase request, String registryAuth) {
        if (StrUtil.isNotBlank(registryAuth)) {
            request.setHeader(REGISTRY_AUTH_HEADER, registryAuth);
        }
        return execute(request);
    }

    @Override
    public <T> T post(URI uri, String contentType, String registryAuth, HttpEntity entity, IResponseProcessHandler<T> handler) {
        return this.post(uri, contentType, registryAuth, null, entity, handler);
    }

    @Override
    public <T> T post(URI uri, String contentType, String registryAuth, Map<String, String> headers, HttpEntity entity, IResponseProcessHandler<T> handler) {
        var request = new HttpPost(uri);
        if (CollectionUtil.isNotEmpty(headers)) {
            for (var entry : headers.entrySet()) {
                request.setHeader(entry.getKey(), entry.getValue());
            }
        }
        if (StrUtil.isNotBlank(registryAuth)) {
            request.setHeader(REGISTRY_AUTH_HEADER, registryAuth);
        }
        if (StrUtil.isNotBlank(contentType)) {
            request.setHeader("Content-Type", contentType);
        }
        if (entity != null) {
            request.setEntity(entity);
        }
        return execute0(request, handler);
    }

    @Override
    public <T> T post(RequestParam param, IResponseProcessHandler<T> handler) {
        var request = new HttpPost(param.getUri());
        if (CollectionUtil.isNotEmpty(param.getHeaders())) {
            for (var entry : param.getHeaders().entrySet()) {
                request.setHeader(entry.getKey(), entry.getValue());
            }
        }
        if (StrUtil.isNotBlank(param.getRegistryAuth())) {
            request.setHeader(REGISTRY_AUTH_HEADER, param.getRegistryAuth());
        }
        if (StrUtil.isNotBlank(param.getContentType())) {
            request.setHeader("Content-Type", param.getContentType());
        }
        if (param.getEntity() != null) {
            request.setEntity(param.getEntity());
        }
        return execute0(request, handler);
    }

    public <T> T execute0(HttpUriRequest request, IResponseProcessHandler<T> handler) {
        beforeExecute(request);
        var content = new HttpCoreContext();
        try {
            var url = request.getUri();
            handler.setHost(this.host.toHostString());
            handler.setUrl(url);
            return this.client.execute(this.host, request, content, handler::process);
        } catch (IOException | URISyntaxException e) {
            throw new DockerConnectionException(this.host.toHostString(), e);
        }

    }

    public Response test(HttpUriRequest request, String contentType, Path path) {
        try {
            beforeExecute(request);
            request.setHeader("Content-Type", contentType);
            var entity = new FileEntity(path.toFile(), ContentType.APPLICATION_OCTET_STREAM);
            request.setEntity(entity);
            var context = new HttpCoreContext();
            this.client.execute(this.host, request, context, response -> {
                int statusCode = response.getCode();
                if (statusCode >= 400 && statusCode <= 500) {
                    byte[] content = readContent(response);
                    response.close();
                    System.out.println(new String(content, StandardCharsets.UTF_8));
                    Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
                    Message message = deserializeMessage(content);

                } else {
                    try (var is = response.getEntity().getContent(); var bs = new BufferedReader(new InputStreamReader(is))) {
                        String streamLine;
                        while ((streamLine = bs.readLine()) != null) {
                            System.out.println(streamLine);
                        }
                    }
                }
                return "hello";
            });
           /* int statusCode = response.getCode();
            if (statusCode >= 400 && statusCode <= 500) {
                byte[] content = readContent(response);
                response.close();
                System.out.println(new String(content, StandardCharsets.UTF_8));
                Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
                Message message = deserializeMessage(content);
                throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode,
                        response.getReasonPhrase(), errors, message);
            }*/
            return null;
            //return new HttpClientResponse(response);
        } catch (IOException ex) {
            throw new DockerConnectionException(this.host.toHostString(), ex);
        }
    }

    private Response execute(HttpUriRequest request) {
        try {
            beforeExecute(request);
            var context = new HttpCoreContext();
            ClassicHttpResponse response = this.client.executeOpen(this.host, request, context);
            int statusCode = response.getCode();
            if (statusCode >= 400 && statusCode <= 500) {
                byte[] content = readContent(response);
                response.close();
                System.out.println(new String(content, StandardCharsets.UTF_8));
                Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
                Message message = deserializeMessage(content);
                throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode,
                        response.getReasonPhrase(), errors, message);
            }
            return new HttpClientResponse(response);
        } catch (IOException | URISyntaxException ex) {
            throw new DockerConnectionException(this.host.toHostString(), ex);
        }
    }

    protected void beforeExecute(HttpRequest request) {
    }

    private byte[] readContent(ClassicHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        if (entity == null) {
            return null;
        }
        try (InputStream stream = entity.getContent()) {
            return (stream != null) ? stream.readAllBytes() : null;
        }
    }

    private Errors deserializeErrors(byte[] content) {
        if (content == null) {
            return null;
        }
        try {
            // return SharedObjectMapper.get().readValue(content, Errors.class);
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

    private Message deserializeMessage(byte[] content) {
        if (content == null) {
            return null;
        }
        try {
            return JSON.parseObject(content, Message.class);
        } catch (Exception ex) {
            return null;
        }
    }

    HttpHost getHost() {
        return this.host;
    }

    /**
     * {@link HttpEntity} to s.
     */
    private static class WritableHttpEntity extends AbstractHttpEntity {

        private final IOConsumer<OutputStream> writer;

        WritableHttpEntity(String contentType, IOConsumer<OutputStream> writer) {
            super(contentType, "UTF-8");
            this.writer = writer;
        }

        @Override
        public boolean isRepeatable() {
            return false;
        }

        @Override
        public long getContentLength() {
            if (this.getContentType() != null && this.getContentType().equals("application/json")) {
                return calculateStringContentLength();
            }
            return -1;
        }

        @Override
        public InputStream getContent() throws UnsupportedOperationException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            this.writer.accept(outputStream);
        }

        @Override
        public boolean isStreaming() {
            return true;
        }

        private int calculateStringContentLength() {
            try {
                ByteArrayOutputStream bytes = new ByteArrayOutputStream();
                this.writer.accept(bytes);
                return bytes.toByteArray().length;
            } catch (IOException ex) {
                return -1;
            }
        }

        @Override
        public void close() throws IOException {
        }

    }

    /**
     * An HTTP operation response.
     */
    private static class HttpClientResponse implements Response {

        private final ClassicHttpResponse response;

        HttpClientResponse(ClassicHttpResponse response) {
            this.response = response;
        }

        @Override
        public InputStream getContent() throws IOException {
            return this.response.getEntity().getContent();
        }

        @Override
        public Header getHeader(String name) {
            return this.response.getFirstHeader(name);
        }

        @Override
        public void close() throws IOException {
            this.response.close();
        }

    }

}
